﻿using Bouyei.NetFactoryCore.Protocols;
using Bouyei.NetFactoryCore.Protocols.Quic;
using Bouyei.NetFactoryCore.Protocols.Quic.Connections;
using Bouyei.NetFactoryCore.Protocols.Quic.Constants;
using Bouyei.NetFactoryCore.Protocols.Quic.Events;
using Bouyei.NetFactoryCore.Protocols.Quic.Frames;
using Bouyei.NetFactoryCore.Protocols.Quic.InternalInfrastructure;
using Bouyei.NetFactoryCore.Protocols.Quic.PacketProcessing;
using Bouyei.NetFactoryCore.Protocols.Quic.Packets;
using Bouyei.NetFactoryCore.Protocols.Quic.Settings;
using Bouyei.NetFactoryCore.Protocols.Quic.Utilities;
using Bouyei.NetFactoryCore.Protocols.Quic.Streams;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;

namespace Bouyei.NetFactoryCore.Quic
{
    /// <summary>
    /// Source of quic module project https://github.com/Vect0rZ/Quic.NET
    /// quic client
    /// </summary>
    public class QuicServerProvider: QuicTransport
    {
        INetServerProvider netProvider = null;
        private readonly Unpacker _unpacker;
        private PacketWireTransfer _pwt;

        private readonly InitialPacketCreator _packetCreator;

        public ClientConnectedEvent OnClientConnected;
        public ConnectionClosedEvent OnClientDisconnected;
        public StreamDataReceivedEvent OnDataReceived;

        public QuicServerProvider(int maxNumberOfConnecions=32)
        {
            netProvider = NetServerProvider.CreateProvider(4096, maxNumberOfConnecions, NetProviderType.Udp);
            netProvider.DisconnectedHandler = Disconnected;
            netProvider.ReceivedOffsetHandler = OnReceived;
            netProvider.AcceptedHandler = OnAccepted;

            _unpacker = new Unpacker();
            _packetCreator = new InitialPacketCreator();
        }

        public bool Start(int port)
        {
            _pwt = new PacketWireTransfer(netProvider, null);
            return netProvider.Start(port);
        }

        public bool Send(QuicStream stream, byte[] data)
        {
           return stream.Send(data);
        }

        public bool Send(QuicConnection connection, byte[] data)
        {
            var stream = connection.CreateStream(StreamType.ServerBidirectional);

            return stream.Send(data);
        }


        private void OnReceived(SegmentToken sToken)
        {
            Packet packet =  _pwt.Read(sToken.Data.buffer, sToken.Data.size, sToken.Data.offset);// _unpacker.Unpack(sToken.Data.buffer, sToken.Data.size, sToken.Data.offset);

            if (packet is InitialPacket)
            {
                QuicConnection connection = ProcessInitialPacket(sToken,packet);
                connection.OnStreamOpened += StreamOpened;
                connection.OnConnectionClosed += StreamClosed;

                OnClientConnected?.Invoke(connection);
            }

            if (packet is ShortHeaderPacket)
            {
                ProcessShortHeaderPacket(packet);
            }
        }

        private void StreamOpened(SocketToken sToken,QuicStream stream)
        {
            stream.OnStreamDataReceived += StreamDataReceived;
        }

        private void StreamClosed(QuicConnection connection)
        {
            OnClientDisconnected?.Invoke(connection);
        }

        private void StreamDataReceived(SocketToken sToken,QuicStream stream, byte[] data)
        {
            OnDataReceived?.Invoke(sToken, stream, data);
        }

        private void Disconnected(SocketToken sToken)
        {

        }

        private void OnAccepted(SocketToken sToken)
        {

        }

        /// <summary>
        /// Processes incomming initial packet and creates or halts a connection.
        /// </summary>
        /// <param name="packet">Initial Packet</param>
        /// <param name="endPoint">Peer's endpoint</param>
        /// <returns></returns>
        private QuicConnection ProcessInitialPacket(SegmentToken sToken, Packet packet)
        {
            QuicConnection result = null;
            UInt64 availableConnectionId;
            byte[] data;
            // Unsupported version. Version negotiation packet is sent only on initial connection. All other packets are dropped. (5.2.2 / 16th draft)
            if (packet.Version != QuicVersion.CurrentVersion || !QuicVersion.SupportedVersions.Contains(packet.Version))
            {
                VersionNegotiationPacket vnp = _packetCreator.CreateVersionNegotiationPacket();
                data = vnp.Encode();

                netProvider.Send(new SegmentToken(sToken.sToken, data));
                return null;
            }

            InitialPacket cast = packet as InitialPacket;
            InitialPacket ip = _packetCreator.CreateInitialPacket(0, cast.SourceConnectionId);

            // Protocol violation if the initial packet is smaller than the PMTU. (pt. 14 / 16th draft)
            if (cast.Encode().Length < QuicSettings.PMTU)
            {
                ip.AttachFrame(new ConnectionCloseFrame(ErrorCode.PROTOCOL_VIOLATION, 0x00, ErrorConstants.PMTUNotReached));
            }
            else if (ConnectionPool.AddConnection(new ConnectionData(new PacketWireTransfer(netProvider, sToken.sToken), sToken.sToken, cast.SourceConnectionId, 0),
                out availableConnectionId) == true)
            {
                // Tell the peer the available connection id
                ip.SourceConnectionId = (byte)availableConnectionId;

                // We're including the maximum possible stream id during the connection handshake. (4.5 / 16th draft)
                ip.AttachFrame(new MaxStreamsFrame(QuicSettings.MaximumStreamId, StreamType.ServerBidirectional));

                // Set the return result
                result = ConnectionPool.Find(availableConnectionId);
            }
            else
            {
                // Not accepting connections. Send initial packet with CONNECTION_CLOSE frame.
                // TODO: Buffering. The server might buffer incomming 0-RTT packets in anticipation of late delivery InitialPacket.
                // Maximum buffer size should be set in QuicSettings.
                ip.AttachFrame(new ConnectionCloseFrame(ErrorCode.CONNECTION_REFUSED, 0x00, ErrorConstants.ServerTooBusy));
            }

            data = ip.Encode();
            netProvider.Send(new SegmentToken(sToken.sToken, data));

            return result;
        }
    }
}
